package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"github.com/gammazero/deque"

	"github.com/aliyun/aliyun-oss-go-sdk/oss"
	log "github.com/sirupsen/logrus"
)

type AliyunBucket struct {
	client *oss.Client
	bucket *oss.Bucket
}

func NewAliyunBucketWithConfig(c AliyunConfigure) (*AliyunBucket, error) {
	var err error
	aliyun := AliyunBucket{}
	aliyun.client, err = oss.New(c.Endpoint, c.AccessKey, c.SecretKey)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("create remote client fail")
		return nil, err
	}
	aliyun.bucket, err = aliyun.client.Bucket(c.Bucket)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("open remote bucket fail")
		return nil, err
	}
	return &aliyun, nil
}

func NewAliyunBucket(endpoint, access_key, secret_key, bucket string) (*AliyunBucket, error) {
	var err error
	aliyun := AliyunBucket{}
	aliyun.client, err = oss.New(endpoint, access_key, secret_key)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("create remote client fail")
		return nil, err
	}
	aliyun.bucket, err = aliyun.client.Bucket(bucket)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("open remote bucket fail")
		return nil, err
	}
	return &aliyun, nil
}

func (self *AliyunBucket) PutObject(key string, content []byte, options ...oss.Option) error {
	err := self.bucket.PutObject(key, bytes.NewReader(content), options...)
	if err != nil {
		log.WithFields(log.Fields{
			"key":   key,
			"error": err.Error(),
		}).Error("aliyun PutObject fail")
	} else {
		log.WithFields(log.Fields{
			"key": key,
		}).Info("aliyun PutObject success")
	}
	return err
}

func (self *AliyunBucket) PutObjectFromFile(key, filepath string, options ...oss.Option) error {
	err := self.bucket.PutObjectFromFile(key, filepath, options...)
	if err != nil {
		log.WithFields(log.Fields{
			"key":      key,
			"filepath": filepath,
			"error":    err.Error(),
		}).Error("aliyun PutObjectFromFile fail")
	} else {
		log.WithFields(log.Fields{
			"key":      key,
			"filepath": filepath,
		}).Info("aliyun PutObjectFromFile success")
	}
	return err
}

func (self *AliyunBucket) GetObject(key string, options ...oss.Option) ([]byte, error) {
	reader, err := self.bucket.GetObject(key, options...)
	if err != nil {
		log.WithFields(log.Fields{
			"key":   key,
			"error": err.Error(),
		}).Error("aliyun GetObject fail")
		return nil, err
	}
	defer reader.Close()
	content, err := io.ReadAll(reader)
	if err != nil {
		log.WithFields(log.Fields{
			"key":   key,
			"error": err.Error(),
		}).Error("aliyun ReadObject fail")
		return nil, err
	}
	return content, nil
}

func (self *AliyunBucket) IsObjectExist(key string) (bool, error) {
	exists, err := self.bucket.IsObjectExist(key)
	if err != nil {
		log.WithFields(log.Fields{
			"key":   key,
			"error": err.Error(),
		}).Error("aliyun stat object fail")
		return false, err
	}
	return exists, nil
}

func (self *AliyunBucket) GetObjectMeta(key string) (Object, error) {
	var result Object
	hdr, err := self.bucket.GetObjectMeta(key)
	if err != nil {
		log.WithFields(log.Fields{
			"key":   key,
			"error": err.Error(),
		}).Error("aliyun get object meta fail")
		return result, err
	}
	result.Key = key
	if last_modified := hdr.Get("Last-Modified"); last_modified != "" {
		tm, err := time.Parse(time.RFC1123, last_modified)
		if err != nil {
			return result, err
		}
		result.LastModified = tm
	}
	if content_length := hdr.Get("Content-Length"); content_length != "" {
		result.Size, err = strconv.ParseInt(content_length, 10, 64)
		if err != nil {
			return result, err
		}
	}
	if x_oss_storage_class := hdr.Get("X-OSS-Storage-Class"); x_oss_storage_class != "" {
		result.StorageClass = x_oss_storage_class
	}
	if x_oss_object_type := hdr.Get("X-OSS-Object-Type"); x_oss_object_type != "" {
		result.Type = x_oss_object_type
	}
	if etag := hdr.Get("ETag"); etag != "" {
		result.ETag = etag
	}
	return result, nil
}

func (self *AliyunBucket) DeleteObject(key string) error {
	_, err := self.bucket.DeleteObjects([]string{key})
	if err != nil {
		log.WithFields(log.Fields{
			"key":   key,
			"error": err.Error(),
		}).Error("aliyun DeleteObjects fail")
		return err
	} else {
		log.WithFields(log.Fields{
			"key": key,
		}).Info("aliyun DeleteObjects success")
	}
	return nil
}

func (self *AliyunBucket) PutJSON(key string, v interface{}) error {
	content, err := json.Marshal(v)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
			"key":   key,
		}).Error("marshal object fail")
		return err
	}
	return self.PutObject(key, content, oss.ContentType("application/json; charset=UTF-8"))
}

func (self *AliyunBucket) CopyObject(src, dst string) error {
	_, err := self.bucket.CopyObject(src, dst)
	ctx := log.WithFields(log.Fields{
		"source":      src,
		"destination": dst,
	})
	if err != nil {
		ctx.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("aliyun CopyObject fail")
		return err
	}
	return nil
}

func (self *AliyunBucket) GetJSON(key string, v interface{}) error {
	content, err := self.GetObject(key)
	if err != nil {
		return err
	}
	return json.Unmarshal(content, v)
}

type Object = oss.ObjectProperties

func (self *AliyunBucket) ListObjects(prefix string) ([]Object, error) {
	ctx := log.WithFields(log.Fields{
		"prefix": prefix,
	})
	objs := make([]Object, 0)
	queue := deque.New[string]()
	queue.PushBack(prefix)
	if prefix == "" {
		return objs, fmt.Errorf("empty prefix")
	}
	for queue.Len() > 0 {
		prefix = queue.PopFront()
		next := ""
		for {
			result, err := self.bucket.ListObjectsV2(oss.Prefix(prefix), oss.Delimiter("/"), oss.ContinuationToken(next))
			if err != nil {
				ctx.WithFields(log.Fields{
					"error": err.Error(),
				}).Error("aliyun list objects fail")
				return objs, err
			}
			for _, ele := range result.Objects {
				if strings.HasSuffix(ele.Key, "/") {
					continue
				}
				objs = append(objs, ele)
			}
			for _, ele := range result.CommonPrefixes {
				queue.PushBack(ele)
			}
			next = result.NextContinuationToken
			if next == "" {
				break
			}
		}
	}
	return objs, nil
}

func (self *AliyunBucket) Close() {
}

/*
func aliyun_RefreshObject(c AliyunConfigure, urls []string) error {
	client, err := cdn.NewClientWithAccessKey(c.Region, c.AccessKey, c.SecretKey)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("create aliyun client fail")
		return err
	}
	defer client.Shutdown()
	client.EnableAsync(1, 1)
	request := cdn.CreateRefreshObjectCachesRequest()
	request.ObjectPath = strings.Join(urls, "\n")
	request.ObjectType = "File"
	result := client.RefreshObjectCachesWithCallback(request, func(response *cdn.RefreshObjectCachesResponse, err error) {
		if err != nil {
			log.WithFields(log.Fields{
				"error": err.Error(),
				"urls":  urls,
			}).Error("aliyun refresh directory fail")
			return
		}
		log.WithFields(log.Fields{
			"urls":            urls,
			"refresh_task_id": response.RefreshTaskId,
			"request_id":      response.RequestId,
			"status_code":     response.GetHttpStatus(),
		}).Info("aliyun refresh object success")
	})
	<-result
	return nil
}

func aliyun_RefreshDirectory(c AliyunConfigure, urls []string) error {
	client, err := cdn.NewClientWithAccessKey(c.Region, c.AccessKey, c.SecretKey)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err.Error(),
		}).Error("create aliyun client fail")
		return err
	}
	defer client.Shutdown()
	client.EnableAsync(1, 1)
	request := cdn.CreateRefreshObjectCachesRequest()
	request.ObjectPath = strings.Join(urls, "\n")
	request.ObjectType = "Directory"
	result := client.RefreshObjectCachesWithCallback(request, func(response *cdn.RefreshObjectCachesResponse, err error) {
		if err != nil {
			log.WithFields(log.Fields{
				"error": err.Error(),
				"urls":  urls,
			}).Error("aliyun refresh directory fail")
			return
		}
		log.WithFields(log.Fields{
			"urls":            urls,
			"refresh_task_id": response.RefreshTaskId,
			"request_id":      response.RequestId,
			"status_code":     response.GetHttpStatus(),
		}).Info("aliyun refresh directory success")
	})
	<-result
	return nil
}
*/
